iT邦幫忙

2022 iThome 鐵人賽

DAY 30
2
Software Development

軟體架構師的自我修養系列 第 30

[Day 30] 特性開關妙無窮

  • 分享至 

  • xImage
  •  

在鐵人賽的最後一天,我想跟大家分享一個軟體開發中的萬金油:特性開關(Feature Toggle)。

為什麼說是萬金油呢?因為正確使用特性開關可以有效提高軟體開發的生產力,無論是更容易部署或是更容易做實驗,特性開關都扮演其中的靈魂角色。重點是,無論什麼樣的軟體、什麼樣的開發流程都適用。

但在介紹特性開關前,我們先來看兩個很常被混肴的概念。到底設定檔(configuration)和特性開關有什麼差別?

首先,不管設定檔存在哪裡,例如:檔案、資料庫或是前一陣子很流行的Consul,設定檔都被視為一種靜態的存在。當系統初始化時,整份設定檔會被載入執行實體的記憶體中以減少不必要的I/O,也就是說,如果設定檔被改變了,所有執行實體都必須要重啟,以便重新初始化。

另一方面,特性開關不是這麼運作的。當執行實體需要用到特性開關時,他會當下去存取開關,也因此能夠拿到最新的狀態,故特性開關比起設定檔更加動態。

讓我們用程式碼來說明。

如果是設定檔,那程式碼會像是下面這樣。

const config = loadConfigFromSomewhere();
function foo() {
    if (config.featureA) doA();
    else doB();
}

然而,特性開關不太一樣。

const connection = initFeatureToggle();
function bar() {
    if (connection.isEnabled("featureA")) doA();
    else doB();
}

正如你看到的,設定檔在loadConfigFromSomewhere之後就固定了,所有需要用到的地方都是直接從記憶體的變數中讀取。但特性開關是透過isEnabled這個函式每次去動態取值,就能夠拿到當下的結果。

特性開關有哪些種類?

當我們了解了特性開關的概念後,讓我們來看特性開關的種類。

https://martinfowler.com/articles/feature-toggles.html

特性開關有四種形態。

  1. 發布開關(Release Toggles)
  2. 維運開關(Ops Toggles)
  3. 實驗開關(Experiment Toggles)
  4. 權限開關(Permission Toggles)

在這張圖中有兩個象限,壽命(longevity)和活性(dynamism)。

  • 活性指的是開關被修改的頻率,越接近右邊表示越常需要變動開關。
  • 壽命是指開關會在程式碼中存活多久,越接近底部表示開關的存在時間越短。

發布開關在藍色區域,也就是相對靜態,因為只有新版本發布的時候才會需要用到,並且當功能確定穩定了就會將發布開關移除。

另一方面,在綠色區域的開關就相對動態,有可能根據各種需求被修改。

發布開關

發布開關是特性開關最常見的形式,目的是為了控制每一次發布的影響範圍。

假設這次發布有一個新的功能,那麼跟這功能相關的程式碼都應該被同一個發布開關包裹著,且預設是關閉。換句話說,這次發布與上次發布的行為會完全一樣。

當發布成功,這個功能的開關可以逐漸根據需求打開。一開始也許只開放給1%的顧客,接著5%、10%繼續下去。

透過這樣的做法,新功能會逐漸開放直到完全上線,這樣的策略也稱為「金絲雀部署」。

或者,開關也可以在發布後直接全開,若是碰到任何問題,那麼就把開關整個關閉以控制受災範圍,這樣的作法又稱為「藍綠部署」。

重要的是,發布開關的生命週期應該要很短,當確定功能穩定後就要盡快從程式碼中移除。如果沒這麼做,程式碼內就會充斥越來越多開關,進而造成維護負擔。

維運開關

維運開關與發布開關不同。發布開關的目的是管理每次發布的範圍,但維運開關是為了管理基建的修改。當基建因為某種原因需要升級或調整,就可以透過維運開關來管理。

舉例來說,如果原本使用的分散式追踪工具是Elastic APM,但因為預算原因想換成開源的jaeger,那麼維運開關就可以用來切換這兩套系統。

直到我們確定能正確維運jaeger前,這兩套追蹤系統會共存一段時間,有可能一半的人用Elastic APM一半的人用jaeger,而維運開關就可以用來控制這個比例。

至於這個維運開關會一直存在直到我們完全使用jaeger的那天,也因此維運開關的壽命會比發布開關長的多了。

另一個使用維運開關的例子是,手動的斷路器(circuit breaker)。在一個高流量的系統,通常會實作流量控制的演算法,一但流量超過設定的閥值就會切斷服務,以避免灌進來的流量把整組系統打掛。

當然,理想情況是斷路器可以根據某個規則自動啟動和關閉,但是這個流量控制演算法非常複雜也很不容易做對,因此在演算法實作完之前都可以透過維運開關來手動控制斷路器。

實驗開關

實驗開關,正如他的名字,是用來做實驗的開關。

當一個功能有兩種不同行為,而我們想知道到底哪個比較有效時,我們就可以運用實驗開關進行切換,這樣的實驗過程稱為A/B測試。

這個開關的操作與發布開關有點類似,但發布開關是使新舊兩行為共存,而實驗開關則是讓兩個新的行為共存。

在做實驗的過程中,甚至可以透過實驗開關帶入額外的實驗參數來使整個實驗過程更加彈性且有效率。

對,你沒看錯,特性開關是能夠攜帶額外參數的,不僅僅只有truefalse。因此,在實驗的過程中,實驗開關扮演一個很重要的角色,無論是行為控制也好,或提供實驗參數也罷,實驗開關都扮演加速實驗的關鍵。

但就像其他開關一樣,當實驗做完後就應該要把實驗開關給移除。

權限開關

這是最後一種開關,但也是擁有最複雜使用情境的開關。從一開始的分布圖我們可以知道,權限開關既動態又長壽。

那麼權限開關到底是個什麼樣的存在?

從我的觀點來說,有兩種權限開關的使用情境,其一是做系統層級的存取控制,另一種則是產品層級的存取控制。

讓我們先從第一種說起,系統層級的存取控制指的是特定功能或特定操作只能開放指定的使用者,因此我們透過權限開關來管理這些受限的功能。有點難想像對吧,讓我提供一個偽碼做例子。

function restrictedFunction() {
    const metadata = {userId, userLevel, userRole};
    if (connection.isEabled("featureA", metadata))
        doFunction();
    else return;
}

從上面的例子可以知道,這個受限功能只開放給特定使用者,要能使用功能必須通過IdLevelRole的驗證,否則就會被跳過。

這樣的開關看起來很穩定,對吧?事實上,正相反。

驗證的條件有可能改動,原先可能只需要IdLevel,但之後有驗證Role需求那條件就會變更。因此,權限開關會根據各種使用情境和需求而改變。

如何正確使用特性開關?

我們已經看過使用特性開關的程式碼長相了。

const connection = initFeatureToggle();
function bar() {
    if (connection.isEnabled("featureA")) doA();
    else doB();
}

當越來越多開關被加入程式碼,就會有越來越多if-else在程式碼中,這其實違反了大部分乾淨代碼的原則。因此,我們應該額外小心使用特性開關並遵守一些基本設計原則。

  1. 只在需要時使用。
  2. 控制開關的總數。
  3. 開關不要互相重疊(巢狀if-else)。
  4. 定期檢視並清除用不到的開關。

此外,用到開關的程式碼終究會變成下面這樣。

function bar() {
    doB();
}

正因如此,讓移除開關變得容易是很重要的。

我推薦使用設計模式中的Factory Method並將具有特性開關的程式碼封裝進工廠方法中。如此一來,移除開關時只需要移除工廠方法的「產品」即可,所有外部的客戶端都不需要更改。

以下是個基本範例。

function factory()
{
    if ( connection.isEnabled("featureA", metadata) ) {
        return new NewHandler();
    } else {
        return new OrigHandler();
    }
}

// qwer.js
factory().doA();
// asdf.js
factory().doB();
// zxcv.js
factory().doC();

將所有判斷特性開關的程式碼都封裝在工廠方法中,外部的客戶端不需要知道拿到的是新處理者還是舊的。當要移除特性開關時,只需要修改工廠方法並把OrigHandler丟掉即可。

function factory()
{
    return new NewHandler();
}

就結果來說,無論qwer.jsasdf.jszxcv.js都不需要更改。

結論

今天我們談論了許多特性開關的使用情境,並且說明該如何正確對待特性開關。我相信不難看出特性開關的確能大大提升寫程式的生產力。

有一個常被提及的使用案例是,使用特性開關時,應用程式想要根據開關修改做出反應,例如,例如開關打開時應用程式主動做某些處理。

這樣的需求可以透過Observer Pattern達成,但我不建議做這種連動。畢竟我們知道,特性開關終究是會被移除的,也因此不應該介入領域模型太深,開關應該扮演的是系統和使用者的協調者。

為什麼要以特性開關為鐵人賽收尾呢?

因為特性開關非常好用,而且無論大型組織或小型組織都很適合,即便小型組織無法開發屬於自己的特性開關系統,現在市面上也有很多開源或企業級方案。既然引入特性開關不困難,又能有諸多好處,何不試試?


上一篇
[Day 29] 快取一致性實戰(下)
下一篇
鐵人賽後記
系列文
軟體架構師的自我修養31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
Taiming
iT邦研究生 5 級 ‧ 2022-09-30 14:02:57

恭喜大大完賽!

lazypro iT邦新手 5 級 ‧ 2022-09-30 15:16:40 檢舉

謝謝,還有很多可以寫呢

0
sixwings
iT邦研究生 3 級 ‧ 2022-09-30 20:45:27

恭喜大大完賽~

我發現我好像也有用過到類似的概念: 如何在平台開發新功能同時不影響原本的使用者?
感謝大大分享特性開關,原來還有這麼多種應用

lazypro iT邦新手 5 級 ‧ 2022-09-30 20:58:48 檢舉

你的那也是其中一種應用方式,只是做成一個服務會更有彈性

我要留言

立即登入留言